﻿namespace BathroomLocator.Services
{
	using System;
	using System.Globalization;
	using System.Linq;
	using System.Net;
	using System.ServiceModel;
	using System.ServiceModel.Activation;
	using System.ServiceModel.Web;
	using System.Web;
	using System.Web.Security;
	using BathroomLocator.Services.Infrastructure;
	using BathroomLocator.Services.MembershipWrappers;
	using Microsoft.WindowsAzure;
	using Microsoft.WindowsAzure.StorageClient;

	[ServiceBehavior(IncludeExceptionDetailInFaults = false)]
	[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
	public class SharedAccessSignatureService : ISharedAccessSignatureService
	{
		private const SharedAccessPermissions ContainerSharedAccessPermissions = SharedAccessPermissions.Write | SharedAccessPermissions.Delete | SharedAccessPermissions.List;

        private const string DefaultContainerName = "bathroompictures";

		private readonly CloudBlobClient cloudBlobClient;
		private readonly HttpContextBase context;
		private readonly RoleProvider roleProvider;
		private readonly IFormsAuthentication formsAuth;
		private readonly IMembershipService membershipService;
		private readonly WebOperationContext webOperationContext;

		public SharedAccessSignatureService()
			: this(null, new HttpContextWrapper(HttpContext.Current), new RoleProviderWrapper(), new FormsAuthenticationService(), new AccountMembershipService(), WebOperationContext.Current)
		{
		}

		public SharedAccessSignatureService(CloudBlobClient cloudBlobClient, HttpContextBase context, RoleProvider roleProvider, IFormsAuthentication formsAuth, IMembershipService membershipService, WebOperationContext webOperationContext)
		{
			if ((context == null) && (HttpContext.Current == null))
			{
				throw new ArgumentNullException("context", "Context cannot be null if not running on a Web context.");
			}

			if ((cloudBlobClient == null) && (GetStorageAccountFromConfigurationSetting() == null))
			{
				throw new ArgumentNullException("cloudBlobClient", "Cloud Blob Client cannot be null if no configuration is loaded.");
			}

			this.cloudBlobClient = cloudBlobClient ?? GetStorageAccountFromConfigurationSetting().CreateCloudBlobClient();
			this.context = context;
			this.roleProvider = roleProvider;
			this.formsAuth = formsAuth;
			this.membershipService = membershipService;
			this.webOperationContext = webOperationContext;
		}

		private string UserId
		{
			get
			{
				string ticketValue = null;

				var cookie = this.context.Request.Cookies[this.formsAuth.FormsCookieName];
				if (cookie != null)
				{
					// From cookie
					ticketValue = cookie.Value;
				}
				else if (this.context.Request.Headers["AuthToken"] != null)
				{
					// From HTTP header
					ticketValue = this.context.Request.Headers["AuthToken"];
				}

				if (!string.IsNullOrEmpty(ticketValue))
				{
					FormsAuthenticationTicket ticket;

					try
					{
						ticket = this.formsAuth.Decrypt(ticketValue);
					}
					catch
					{
						throw new WebFaultException<string>("The authorization ticket cannot be decrypted.", HttpStatusCode.Unauthorized);
					}

					if (ticket != null)
					{
						var identity = new FormsIdentity(ticket);

						// Authorize blobs usage
						var name = identity.Name;
						if (!this.roleProvider.IsUserInRole(name, RoleConstants.BlobsUsageRole))
						{
							throw new WebFaultException<string>("You have no permission to use blobs.", HttpStatusCode.Unauthorized);
						}

						return this.membershipService.GetUser(name).ProviderUserKey.ToString();
					}
					else
					{
						throw new WebFaultException<string>("The authorization token is no longer valid.", HttpStatusCode.Unauthorized);
					}
				}
				else
				{
					throw new WebFaultException<string>("Resource not found.", HttpStatusCode.NotFound);
				}
			}
		}

		public Uri GetContainerSharedAccessSignature()
		{
			// Authenticate
			var userId = this.UserId;

			try
			{
                var container = this.GetContainer(DefaultContainerName);

				var containerSASExperiationTime = int.Parse(ConfigReader.GetConfigValue("ContainerSASExperiationTime"), NumberStyles.Integer, CultureInfo.InvariantCulture);
				var sas = container.GetSharedAccessSignature(new SharedAccessPolicy()
				{
					Permissions = ContainerSharedAccessPermissions,
					SharedAccessExpiryTime = DateTime.UtcNow + TimeSpan.FromMinutes(containerSASExperiationTime)
				});

				this.webOperationContext.OutgoingResponse.Headers.Add("Cache-Control", "no-cache");

				var uriBuilder = new UriBuilder(container.Uri) { Query = sas.TrimStart('?') };
				return uriBuilder.Uri;
			}
			catch (Exception exception)
			{
				throw new WebFaultException<string>(exception.Message, HttpStatusCode.InternalServerError);
			}
		}

		public Models.CloudBlobCollection GetBlobsSharedAccessSignatures(string blobPrefix, bool useFlatBlobListing)
		{
			// Authenticate
			var userId = this.UserId;

			if (!string.IsNullOrEmpty(blobPrefix))
			{
				blobPrefix = blobPrefix.TrimStart('/', '\\').Replace('\\', '/');
			}

			try
			{
				// The container name for the user is the same as the UserId.
                SetReadOnlySharedAccessPolicy(this.GetContainer(DefaultContainerName));

                var prefix = string.Format(CultureInfo.InvariantCulture, "{0}/{1}", DefaultContainerName, blobPrefix);

				var blobs = this.cloudBlobClient.ListBlobsWithPrefix(prefix, new BlobRequestOptions { UseFlatBlobListing = useFlatBlobListing });
				var result = new Models.CloudBlobCollection
				{
					CloudBlobs = blobs
									.Where(b => b is CloudBlob)
									.Select(
									b => b.ToModel(userId, this.cloudBlobClient.Credentials.AccountName))
									.ToArray()
				};

				this.webOperationContext.OutgoingResponse.Headers.Add("Cache-Control", "no-cache");

				return result;
			}
			catch (Exception exception)
			{
				throw new WebFaultException<string>(exception.Message, HttpStatusCode.InternalServerError);
			}
		}

		private static void SetReadOnlySharedAccessPolicy(CloudBlobContainer container)
		{
			var blobSASExperiationTime = int.Parse(ConfigReader.GetConfigValue("BlobSASExperiationTime"), NumberStyles.Integer, CultureInfo.InvariantCulture);
			var permissions = container.GetPermissions();
			var options = new BlobRequestOptions
			{
				// Fail if someone else has already changed the container before we do.
				AccessCondition = AccessCondition.IfMatch(container.Properties.ETag)
			};
			var sharedAccessPolicy = new SharedAccessPolicy
			{
				Permissions = SharedAccessPermissions.Read,
				SharedAccessExpiryTime = DateTime.UtcNow + TimeSpan.FromDays(blobSASExperiationTime)
			};

			permissions.SharedAccessPolicies.Remove("readonly");
			permissions.SharedAccessPolicies.Add("readonly", sharedAccessPolicy);

			container.SetPermissions(permissions, options);
		}

		private static CloudStorageAccount GetStorageAccountFromConfigurationSetting()
		{
			CloudStorageAccount account = null;

			try
			{
				account = CloudStorageAccount.FromConfigurationSetting("DataConnectionString");
			}
			catch (InvalidOperationException)
			{
				account = null;
			}

			return account;
		}

        private CloudBlobContainer GetContainer(string containerName)
		{
            var container = this.cloudBlobClient.GetContainerReference(containerName.ToLowerInvariant());
			container.CreateIfNotExist();

            // Allow anonymous read access to blobs within container.
            container.SetPermissions(new BlobContainerPermissions { PublicAccess = BlobContainerPublicAccessType.Blob });

			return container;
		}
	}
}
